Skip to content

Receiver-side price floor for single-node store admissions#176

Open
grumbach wants to merge 3 commits into
WithAutonomi:mainfrom
grumbach:single-node-price-floor
Open

Receiver-side price floor for single-node store admissions#176
grumbach wants to merge 3 commits into
WithAutonomi:mainfrom
grumbach:single-node-price-floor

Conversation

@grumbach

Copy link
Copy Markdown
Collaborator

Summary

Node-side companion to ant-client #150 (one-quote uploads). ADR-0004 gives every quote a price ceiling (price = f(committed key count)); removing the old local floor (b52f4577) left no revenue floor. Since the verifier accepts 1..=7 quote proofs and fully validates only the median candidate, a modified client can fetch quotes from the whole neighbourhood and settle only the cheapest valid one. The flexible bundles from #136 were safe while the old floor stood; the combination is the hole, and one-quote clients make it routine.

This PR restores the floor on the receiver, priced from the receiver's own live commitment:

settled_amount >= 3 x tolerance% x calculate_price(local committed responsible key count)

alongside the existing settled >= 3 x median check. The floor compares the settled amount, so an honest client may overpay a cheap quote to clear stricter receivers.

Design points

  • Same pricing basis as our own quotes. The floor reads the commitment via a new non-mutating CommitmentSource::current_binding_snapshot() — the exact source QuoteGenerator prices from, wired in AntProtocol::attach_commitment_source. Never current_chunks() (the unlike-counts comparison that false-rejected honest quotes under the pre-ADR-0004 floor), and never current_binding_for_quote() (which would extend commitment answerability as a side effect).
  • No commitment = baseline floor. A fresh/retired/restarting receiver (or one with nothing attached) prices the floor at calculate_price(0), making it vacuous rather than an outage mode.
  • Shadow-first rollout. Default is observe-only: one telemetry line per evaluated admission (target ant_node::payment::price_floor) with paid price, settled amount, local count/price, and the would-reject verdict. Enforcement is a per-node canary opt-in via ANT_PRICE_FLOOR_ENFORCE=1 (tolerance override ANT_PRICE_FLOOR_TOLERANCE_PERCENT, default 50%); unsetting it is the kill switch. Clients need 4-of-20 stores, so canary enforcement cannot fail honest uploads while telemetry calibrates the tolerance.
  • Scoped to fresh economics only. New VerificationContext::FreshReplication splits fan-out from direct PUT — verified identically everywhere (store-strength cache, cross-check, first-audit) so behaviour is unchanged; the floor applies to both (a single cheap accepting node must not fan a below-floor proof around everyone else's floor), and never to paid-list admission, repair, or cache hits. No historical receipt is repriced. Merkle proofs untouched.
  • Economic decision only. A floor rejection is a payment decline; it feeds no trust/misbehaviour scoring.

Tests

  • Cheapest-of-K adversarial gate: a 3x-baseline settlement against a fuller receiver is rejected under enforcement, on both direct PUT and fresh replication.
  • Exactly-at-floor overpayment accepted; shadow mode never rejects; no-commitment receivers stay vacuous; paid-list admission never repriced.
  • Full lib suite (692 tests), clippy (all targets + -D warnings), fmt, and docs clean.

Rollout

  1. Ship in shadow mode fleet-wide, watch price_floor telemetry for the settled/local-price ratio distribution.
  2. Enable enforcement on a few canary nodes, confirm zero honest rejections.
  3. Widen enforcement ahead of the one-quote client becoming the common path; tune tolerance from data.
  4. Follow-up: companion ADR documenting the floor as its own economic policy (it amends ADR-0004's "a node may always charge less").

…ions

ADR-0004 binds every quote's price to its issuer's committed key count — a
price CEILING. Removing the old local floor (b52f457) left no revenue floor
at all: the node verifier accepts 1..=7 quote proofs and fully validates only
the median-priced candidate, so a modified client can fetch quotes from the
whole neighbourhood, settle only the cheapest valid one 3x, and every receiver
accepts. The flexible-bundle change (WithAutonomi#136) was safe while the old floor stood;
their combination is the hole, and the one-quote client (ant-client WithAutonomi#150)
makes that path routine.

Add the floor half, priced from the SAME live commitment the receiver's own
QuoteGenerator uses (committed responsible key count via a new non-mutating
CommitmentSource::current_binding_snapshot — never current_chunks(), whose
divergence from commitment counts is what false-rejected honest quotes under
the pre-ADR-0004 floor; and never current_binding_for_quote(), which would
extend answerability). Requirement on single-node store admissions:

    settled >= 3 x tolerance% x calculate_price(local committed key count)

alongside the existing settled >= 3 x median check, i.e. an honest client may
overpay a cheap quote to clear stricter receivers. No live commitment (fresh
node, retired, restart window, unwired) prices the floor at baseline — vacuous,
never an outage mode.

Shadow-first rollout: default is observe-only with one telemetry line per
evaluated admission (target ant_node::payment::price_floor) recording paid
price, settled amount, local count/price, and the would-reject verdict.
Enforcement is per-node via ANT_PRICE_FLOOR_ENFORCE=1 for canary rollout
(tolerance override: ANT_PRICE_FLOOR_TOLERANCE_PERCENT, default 50); unsetting
it is the kill switch. Clients need 4-of-20 stores, so canary enforcement
cannot fail honest uploads while telemetry calibrates the tolerance. A floor
rejection is an economic admission decision only, never trust evidence.

Scope: split VerificationContext::FreshReplication out of ClientPut — verified
identically everywhere (store-strength cache, cross-check, first-audit) so the
floor's telemetry can distinguish direct ingress from fan-out and the fan-out
policy stays tunable without touching direct PUTs. The floor applies to both
(one cheap accepting node must not fan a below-floor proof around everyone
else's floor) and NEVER to paid-list admission, repair, or cache hits — no
historical receipt is repriced. Merkle proofs are untouched.

Tests: cheapest-of-K settlement rejected under enforcement (direct PUT and
fresh replication), exactly-at-floor overpayment accepted, shadow mode never
rejects, no-commitment receivers stay baseline-vacuous, paid-list admission
never repriced.
Copilot AI review requested due to automatic review settings July 14, 2026 05:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a receiver-side “price floor” policy to single-node payment verification so a node can reject (or shadow-log) underpaid store admissions even when a client settles only the cheapest valid quote from a bundle.

Changes:

  • Introduces PriceFloorConfig (shadow by default; env-controlled enforcement + tolerance) and applies it during single-node proof verification for store-admission contexts.
  • Splits VerificationContext with a new FreshReplication variant and unifies cache/admission semantics via is_store_admission().
  • Adds CommitmentSource::current_binding_snapshot() and wires the same commitment source into both quoting and the verifier’s price-floor computation; adds unit/e2e config wiring and tests.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/e2e/testnet.rs Adds default price_floor config into e2e testnet verifier construction.
tests/e2e/data_types/chunk.rs Wires PriceFloorConfig::default() into verifier config used by chunk e2e tests.
src/storage/handler.rs Forwards the attached commitment source to the payment verifier (in addition to the quote generator).
src/replication/mod.rs Uses VerificationContext::FreshReplication for fresh-offer payment verification and updates the associated test.
src/replication/commitment_state.rs Implements current_binding_snapshot() for ResponderCommitmentState without mutating answerability.
src/payment/verifier.rs Implements PriceFloorConfig, adds store-admission context predicate, enforces/logs receiver-side floor after settlement verification, and adds targeted tests.
src/payment/quote.rs Extends CommitmentSource trait with current_binding_snapshot() and updates test implementation.
src/payment/mod.rs Re-exports PriceFloorConfig and the env var names for external configuration.
src/node.rs Initializes verifier price_floor from env and logs when enforcement is enabled.
src/devnet.rs Sets default price_floor config for devnet verifier construction.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/payment/verifier.rs
Comment on lines +1106 to +1110
let local_key_count = source
.and_then(|s| s.current_binding_snapshot())
.map_or(0u32, |binding| binding.key_count);
let local_price = calculate_price(usize::try_from(local_key_count).unwrap_or(usize::MAX));

The floor is its own economic policy, not part of ADR-0004's ceiling: it
amends "a node may always charge less" to "a receiver may refuse a payment
that settles below its own commitment-priced floor". Records the pricing
basis (committed responsible count via non-mutating snapshot), the scope
(store admissions only, no historical repricing), the shadow-first rollout
with per-node canary enforcement, and the validation gates.
Copilot AI review requested due to automatic review settings July 14, 2026 05:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Adds a live-Anvil integration suite (poc_price_floor_live) that exercises the
REAL production paths the unit-test overrides cannot: a real deployed payment
vault paid with a real on-chain pay_for_quotes, read back through the
production completedPayments contract call, against the real
ResponderCommitmentState as the floor's commitment source. Proves shadow
accepts a below-floor settlement, enforcement rejects the cheapest-of-K
one-quote settlement, an at-floor overpayment is accepted, and reading the
floor does not mutate commitment state.

Hardening from adversarial xhigh review (verdict: GO for shadow deployment;
no admission-logic fix required):

- from_env now FAILS CLOSED on a present-but-invalid tolerance: a malformed or
  out-of-range ANT_PRICE_FLOOR_TOLERANCE_PERCENT can no longer leave
  enforcement on with a silently-defaulted 50% (it disables enforcement and
  logs an error). Unit test covers unset/valid/out-of-range/unparseable.
- Extend the two 'every context' regression tests to include FreshReplication,
  matching the store-admission predicate.
- Correct stale comments that still claimed the floor reads current_chunks() /
  the on-disk record count (handler.rs, verifier.rs storage field + attach doc).
- Soften the 'never an outage' wording: a no-commitment baseline floor clears
  every on-curve honest 3x settlement; only an off-curve sub-baseline quote
  (rejected by the ADR-0004 arithmetic gate once enforced) could fall below it.
Copilot AI review requested due to automatic review settings July 15, 2026 05:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comment thread src/payment/verifier.rs
/// Idempotent: calling twice replaces the handle.
pub fn attach_storage(&self, storage: Arc<LmdbStorage>) {
*self.storage.write() = Some(storage);
debug!("PaymentVerifier: LmdbStorage attached for paid-quote price-floor checks");
Comment thread src/payment/verifier.rs
Comment on lines +3474 to +3477
fn price_floor_from_env_fails_closed_on_invalid_tolerance() {
// Defaults with nothing set.
std::env::remove_var(PRICE_FLOOR_ENFORCE_ENV);
std::env::remove_var(PRICE_FLOOR_TOLERANCE_ENV);
.map(|i| {
let mut k = [0u8; 32];
k[..4].copy_from_slice(&i.to_be_bytes());
(k, *blake3::hash(&k).as_bytes())

@dirvine dirvine left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — receiver-side price floor

Reviewed current head bfe16d733c9be1ca887db3097b2e7acbb9a3d52b with six independent passes and local verification.

Verdict: no protocol/security blocker for merging the default-shadow implementation. This is not approval to enable enforcement yet.

What holds

  • ClientPut and FreshReplication both use store-strength verification and floor evaluation; PaidListAdmission, repair, Merkle proofs and existing store-cache hits remain deliberately exempt.
  • Paid-list cache entries cannot satisfy store admission, and insertion happens only after the authenticated paid quote, on-chain settlement and floor checks succeed.
  • The floor uses the receiver's non-mutating live commitment snapshot and compares the actual settled amount, not the cheap quote price.
  • Commitment rotation/snapshot locking is sound; no context/cache downgrade bypass was found.
  • The focused local floor suite passed: cargo test price_floor --lib --features test-utils7/7.
  • Current GitHub checks are all green across builds, platform tests, clippy, fmt, docs, audit, ADR validation and no-logging tests.

Rollout gates before any canary enforcement

  1. Make shadow evidence observable. ant_node::payment::price_floor is an info! event, but normal startup installs no subscriber unless --enable-logging / ANT_ENABLE_LOGGING is set, and no-logging builds cannot emit it. Confirm a logging-enabled artefact and service configuration before calling shadow collection active.
  2. Make the signal queryable. The values are currently embedded in one high-volume formatted string containing the full XOR name. Prefer structured low-cardinality fields/counters/histograms (separate ClientPut and FreshReplication) or publish and test the exact collector parser/query.
  3. Run the live settlement suite with retained evidence. poc_price_floor_live genuinely covers a real Anvil payment, production completedPayments read and real commitment state, but no PR workflow executes it. Add one Ubuntu CI invocation, or run and retain equivalent evidence, before enforcement. It deliberately bypasses live DHT issuer locality and production startup/network wiring, so describe it as a focused vault/floor integration test rather than a full network E2E.
  4. Soften the 4-of-20 claim. Compatible clients require four acceptances and can fall back across up to twenty discovered targets; this gives headroom but does not guarantee success when fewer targets are usable or enforcing nodes are XOR-local. Replace “cannot fail honest uploads” with a conditional statement and define the maximum canary count.
  5. Document rollback accurately. The environment is read once at startup. The kill switch is: unset ANT_PRICE_FLOOR_ENFORCE, restart the named canary, then positively verify shadow mode. Unsetting alone does nothing to a running node.
  6. Define the shadow window, sample size, rejection bound, canary count, stop condition and sign-off owner in ADR-0006 before moving it from Proposed or widening enforcement. “~0” is not an operational threshold.

Minor, non-blocking notes

  • The arithmetic computes (local_price × tolerance / 100) × 3; the written formula can be read as local_price × tolerance × 3 / 100. The difference is at most two atomic units, but code/ADR/tests should name one canonical rounding rule.
  • Present-but-non-Unicode tolerance values are treated as absent, and unrecognised enforcement values silently select shadow mode. Availability-safe, but an explicit effective-config startup signal would make operator mistakes visible.
  • The existing process-global environment test would be safer behind a static mutex.

Review action: comment only — no changes requested, no approval/merge action. Human review is still required for the Proposed ADR and enforcement policy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants